package com.pinfly.common.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.AccessControlException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * ConfigXMLReader This class will read an XML file and create a structure than
 * can be parsed for certain configuration values by moving the index to
 * particular elements.
 * 
 * NOTE:
 * 
 * XML FILE TERMINOLOGY <elementname><elementname attributename=attributevalue
 * attributename1=attributevalue1> </elementname> <elementname>elementtext
 * </elementname> </elementname>
 * 
 */
public class ConfigXMLReader
{
    private static final Logger logger = Logger.getLogger (ConfigXMLReader.class);
    private static final String directoryInJarFile = "properties/config/";

    private String m_fileName;
    private boolean m_useJar = false;
    private String m_fullName = null;
    private String m_noCountryName = null;
    private String m_noLangName = null;
    private CRElement m_rootElement = null;
    private CRElement m_index;

    // Filepath separator
    private static String s_fileSep = System.getProperty ("file.separator");

    // Constructor
    /**
     * ConfigReader
     * 
     * @param fileName The name of the configuration file to read. The file name
     *            is assumed to be in the format xxx_ll_CC.xml, where ll
     *            represents the desired language and CC the desired country,
     *            for example props_en_US.xml. The file is searched for in the
     *            config props directory, then the props directory, then the
     *            current working diretory, then directoryInJarFile directory in
     *            jar file which includes the class. In each directory it first
     *            looks for the complete file name. If that is not found, it
     *            strips off the country. If that is not found it strips off the
     *            language. So using props_en_US.xml as the example, it first
     *            looks for props_en_US.xml, the for props_en.xml and finally
     *            for props.xml. It takes the first file it finds. Note that the
     *            directories have a higher priority than the file name, so that
     *            a generic file (props.xml) in the config props directory is
     *            preferred over a specific file (props_en_US.xml) in the
     *            current working directory.
     */
    public ConfigXMLReader (String fileName)
    {
        if (fileName == null)
        {
            throw new NullPointerException ("fileName argument cannot be null");
        }
        // Default is just the input parameter;
        m_fileName = fileName;

        // Determine the three file names we'll search for
        m_fullName = m_fileName;

        // Need to pull off the suffix for the rest of the processing
        int lastDot = fileName.lastIndexOf ('.');
        String suffix = "";
        String baseName = "";
        if (lastDot != -1)
        {
            // There was a suffix
            // For convenience, the suffix includes the .
            suffix = fileName.substring (lastDot);
            baseName = fileName.substring (0, lastDot);
        }
        else
        {
            // No suffix. The base name is just the full name
            baseName = fileName;
        }
        // Strip off the country
        int us = baseName.lastIndexOf ('_');
        {
            if (us != -1)
            {
                m_noCountryName = baseName.substring (0, us);
                // Now strip off the language
                us = m_noCountryName.lastIndexOf ('_');
                if (us != -1)
                {
                    m_noLangName = m_noCountryName.substring (0, us) + suffix;
                }
                // Add the suffix to the noCountryName.
                // Couldn't do this until we had removed the language
                m_noCountryName += suffix;
            }
        }

        boolean fileExist = false;

        try
        {
            // Setup the directories to search. If an AccessControlException
            // is thrown that probably means we are running in an Applet,
            // so just store the name and read the files in the Parse Method.
            String[] dirNames = new String[]
            { System.getProperty ("pinfly.props.config.dir"), System.getProperty ("pinfly.props.dir"),
             System.getProperty ("user.dir") };

            if (logger.isDebugEnabled ())
            {
                StringBuffer buff = new StringBuffer ();
                buff.append ("Looking in");
                for (int i = 0; i < dirNames.length; i++)
                {
                    buff.append (' ').append (dirNames[i]);
                }
                buff.append (' ').append (directoryInJarFile);
                buff.append ("\n for ").append (m_fullName);
                if (m_noCountryName != null)
                {
                    buff.append (", ").append (m_noCountryName);
                    if (m_noLangName != null)
                    {
                        buff.append (", ").append (m_noLangName);
                    }
                }
                logger.debug (buff.toString ());
            }

            for (int x = 0; x < dirNames.length; x++)
            {
                // Check for the full name
                File f = new File (dirNames[x] + s_fileSep + m_fullName);
                if (f.exists ())
                {
                    m_fileName = f.getPath ();
                    fileExist = true;
                    break;
                }
                if (m_noCountryName != null)
                {
                    // The name without a country
                    f = new File (dirNames[x] + s_fileSep + m_noCountryName);
                    if (f.exists ())
                    {
                        m_fileName = f.getPath ();
                        fileExist = true;
                        break;
                    }
                    if (m_noLangName != null)
                    {
                        // And finally the name without a language
                        f = new File (dirNames[x] + s_fileSep + m_noLangName);
                        if (f.exists ())
                        {
                            m_fileName = f.getPath ();
                            fileExist = true;
                            break;
                        }
                    }
                }
            }
        }
        catch (AccessControlException ace)
        {
            logger.debug ("Unable to access system properties, trying jar file.");
        }

        // if don't find xml files in the above directory, ConfigXMLReader class
        // will find
        // the xml files in directoryInJarFile
        if (!fileExist)
        {
            m_useJar = true;
        }

    }

    // Private Methods

    /**
     * parseElement This method parse an element it is called recursively.
     * 
     * @param elNode the element node to parse
     * @return a CRElement for the Element
     */
    private CRElement parseElement (Element elNode)
    {
        NodeList nl = null;
        Node aNode = null;
        String name = null;
        NamedNodeMap attr = null;
        String attrName = null;
        String attrValue = null;
        short nodeType;

        // Get the Element name
        name = elNode.getTagName ();

        // Create a CRElement object of this element
        CRElement element = new CRElement (name);
        // Get all the attributes of the element
        // and add them to the CRElement object
        attr = elNode.getAttributes ();
        for (int x = 0; x < attr.getLength (); x++)
        {
            aNode = attr.item (x);
            attrName = aNode.getNodeName ();
            attrValue = aNode.getNodeValue ();
            element.addAttr (attrName, attrValue);
        }

        // Get all the child nodes of the element
        // Recursively call parseElement with the child if
        // its an element.
        nl = elNode.getChildNodes ();
        for (int x = 0; x < nl.getLength (); x++)
        {
            aNode = nl.item (x);
            nodeType = aNode.getNodeType ();
            if (nodeType == Node.ELEMENT_NODE)
                element.addElement (parseElement ((Element) aNode));
            else if (nodeType == Node.TEXT_NODE)
                element.setElementText (aNode.getNodeValue ());

        }

        return element;
    }

    /**
     * printElement Returns a string containing the information in an element.
     * 
     * @param el the element to print
     * @return a String representation of the element
     */
    private String printElement (CRElement el)
    {
        StringBuffer buff = new StringBuffer ();
        CRElement[] elements = null;
        CRElement cr = null;
        String text;

        // Get the element name
        buff.append ("<" + el.getElementName () + "> ");
        text = el.getElementText ();
        if (text != null)
            buff.append (text + "\n");
        // Get the attributes of the element
        buff.append (el.getAttributes ());
        if (el.hasElements ())
        {
            // Get the child elements
            elements = el.getElements ();
            for (int x = 0; x < elements.length; x++)
            {
                cr = elements[x];
                buff.append (printElement (cr));
            }
        }
        buff.append ("</" + el.getElementName () + ">");
        return buff.toString ();
    }

    // Public Methods

    /**
     * reset Reset the index element pointer to the root element.
     * 
     * @return a boolean of true if successful, false otherwise
     */
    public boolean reset ()
    {
        if (m_rootElement == null)
            return false;

        m_index = m_rootElement;
        return true;
    }

    /**
     * setElement Sets the index element pointer to a specific element of the
     * specified name.
     * 
     * @param name the element name to point to
     * @return true if the element is found, false otherwise
     */
    public boolean setElement (String name)
    {
        CRElement el = m_index.getElement (name);
        if (el != null)
        {
            m_index = el;
            return true;
        }
        return false;
    }

    /**
     * setElementWithAttribute Sets the index element pointer to a specific
     * element that contains an attribute with the specified value.
     * 
     * @param attrName the attribute name to use to find the element
     * @param attrValue the attribute value to use to find the element
     * @return true if the element was found, false otherwise
     */
    public boolean setElementWithAttribute (String attrName, String attrValue)
    {
        CRElement el = m_index.getElementWithAttribute (attrName, attrValue);
        if (el != null)
        {
            m_index = el;
            return true;
        }
        return false;
    }

    /**
     * setNamedElementWithAttribute Sets the index element pointer to a specific
     * element that contains an attribute with the specified value.
     * 
     * @param elName the name of the element
     * @param attrName the attribute name
     * @param attrValue the attribute value
     * @return true if successful, false otherwise
     */
    public boolean setNamedElementWithAttribute (String elName, String attrName, String attrValue)
    {
        CRElement el = m_index.getNamedElementWithAttribute (elName, attrName, attrValue);
        if (el != null)
        {
            m_index = el;
            return true;
        }
        return false;
    }

    /**
     * getElementName Returns the name of the current element.
     * 
     * @return the name of the current element as a String
     */
    public String getElementName ()
    {
        return m_index.getElementName ();
    }

    /**
     * getElementText Returns the element Text of the current element.
     * 
     * @return the text of the current element as a String
     */
    public String getElementText ()
    {
        return m_index.getElementText ();
    }

    /**
     * getChildElementNames Returns a string array of the element names.
     * 
     * @return the names of child elements of the current element as an array of
     *         Strings
     */
    public String[] getChildElementNames ()
    {
        return m_index.getElementNames ();
    }

    /**
     * getAttributeNames Returns a String array of the attribute names at the
     * current element.
     * 
     * @return the attribute names for the current element as an array of
     *         Strings
     */
    public String[] getAttributeNames ()
    {
        return m_index.getAttributeNames ();
    }

    /**
     * getAttributeValue Returns the value of the specified attribute.
     * 
     * @param name the attribute name in the current element
     * @return the attribute value for the given attribute name of the current
     *         element
     */
    public String getAttributeValue (String name)
    {
        return m_index.getAttribute (name);
    }

    /**
     * getAttributeValuesOfElementNamed Returns a String array of the values of
     * the specified elements and specified attribute name.
     * 
     * @param elName the name of the element to search for
     * @param attrName the attribute name to search for
     * @return an array of String values for the attribute in the element
     */
    public String[] getAttributeValuesOfElementNamed (String elName, String attrName)
    {
        return m_index.getAttributeValuesOfElementNamed (elName, attrName);
    }

    /**
     * parseXmlDoc Parse's the xml document and created the internal CRElement
     * objects.
     */
    public void parseXmlDoc ()
    {
        Document doc = null;
        Element docElement = null;
        BufferedReader in = null;
        InputStream is = null;

        try
        {

            if (m_useJar)
            {
                ClassLoader classLoader = Thread.currentThread ().getContextClassLoader ();
                if (classLoader == null)
                {
                    classLoader = this.getClass ().getClassLoader ();
                }

                // Check for the full name
                is = classLoader.getResourceAsStream (directoryInJarFile + m_fullName);
                if ((is == null) && (m_noCountryName != null))
                {
                    // The name without a country
                    is = classLoader.getResourceAsStream (directoryInJarFile + m_noCountryName);
                    if ((is == null) && (m_noLangName != null))
                    {
                        // And finally the name without a language
                        is = classLoader.getResourceAsStream (directoryInJarFile + m_noLangName);
                    }
                }
                if (is != null)
                {
                    in = new BufferedReader (new InputStreamReader (is));
                }
            }
            else
            {
                in = new BufferedReader (new FileReader (new File (m_fileName)));
            }

            doc = DocumentBuilderFactory.newInstance ().newDocumentBuilder ().parse (new InputSource (in));
            docElement = doc.getDocumentElement ();
            m_rootElement = parseElement (docElement);
            m_index = m_rootElement;
        }
        catch (Exception e)
        {
            logger.fatal ("Unable to parse XML document: " + m_fileName, e);
        }
        finally
        {
            if (in != null)
            {
                try
                {
                    in.close ();
                }
                catch (Exception e)
                {
                }
            }

            if (is != null)
            {
                try
                {
                    is.close ();
                }
                catch (Exception e)
                {
                }
            }
        }
    }

    /**
     * printDoc This method will display the internal structure of the XML
     * document for validation and testing purposes.
     */
    public void printDoc ()
    {

        System.out.println ("XML File: " + m_fileName);
        System.out.println (printElement (m_rootElement));
        System.out.println ("Completed.");

    }

    public static void main (String[] args)
    {
        ConfigXMLReader cr = new ConfigXMLReader ("c:/conf/cfg_en_US.xml");
        cr.parseXmlDoc ();
        cr.printDoc ();
    }

}

/**
 * CRElement This internal class respresents an XML Document Element. For
 * Example: <apps> - CRElement <app id="App1" - CRElement, Attribute size="1"> -
 * Attribute </app> <app id="App2" - CRElement, Attribute size="2"> - Attribute
 * </app> </apps>
 */
class CRElement
{

    private Hashtable _attributes = new Hashtable (); // Unique Attribute Names
    private Vector _elements = new Vector (); // Child Elements
    private String _name = null; // Element name
    private String _text = null; // Element Text

    /**
     * CRElement Constructor
     * 
     * @param the name of the element
     */
    public CRElement (String name)
    {
        _name = name;
    }

    /**
     * hasElements This method returns a true if the element has any child
     * elements.
     * 
     * @return true if the element has children, false otherwise
     */
    public boolean hasElements ()
    {
        if (_elements.size () > 0)
            return true;
        return false;
    }

    /**
     * setElementText This method sets the element Text.
     * 
     * @param the text to set as a String
     */
    public void setElementText (String value)
    {
        _text = value;
    }

    /**
     * addAttr This method adds an attribute to the element.
     * 
     * @param key the name of the attribute
     * @param value the value for the attribute
     */
    public void addAttr (String key, String value)
    {
        _attributes.put (key, value);
    }

    /**
     * addElement This method adds a new Child Element.
     * 
     * @param el the element to add as a child
     */
    public void addElement (CRElement el)
    {
        _elements.addElement (el);
    }

    /**
     * getElementName Returns the name of the element.
     * 
     * @return the name of the current element as a String
     */
    public String getElementName ()
    {
        return _name;
    }

    /**
     * getElementText Returns the element text.
     * 
     * @return the text of the element as a String
     */
    public String getElementText ()
    {
        return _text;
    }

    /**
     * getAttributeValuesOfElementNamed Returns a String array of the attribute
     * values associated with the specified name and attribute name.
     * 
     * @param elName the name of the element
     * @param attrName the name of the attribute
     * @return an array of values for the attribute as Strings
     */
    public String[] getAttributeValuesOfElementNamed (String elName, String attrName)
    {
        CRElement[] el = getElements ();
        Vector v = new Vector ();
        String val;
        String[] vals = null;

        if (el == null)
            return null;

        for (int x = 0; x < el.length; x++)
        {
            if (el[x].getElementName ().equals (elName))
            {

                val = el[x].getAttribute (attrName);
                if (val != null)
                    v.addElement (val);
            }
        }

        if (v.size () > 0)
        {
            vals = new String[v.size ()];
            v.copyInto (vals);
            return vals;
        }
        else
            return null;

    }

    /**
     * getAttributeNames Returns a String array of the attribute names of this
     * element.
     * 
     * @return the names of the attributes for this element
     */
    public String[] getAttributeNames ()
    {
        String[] names = new String[_attributes.size ()];
        int count = 0;
        Enumeration e = _attributes.keys ();
        while (e.hasMoreElements ())
        {
            names[count] = (String) e.nextElement ();
            count++;
        }
        return names;
    }

    /**
     * getElements Returns an array of the child elements.
     * 
     * @return an array of child elements for this element
     */
    public CRElement[] getElements ()
    {
        if (!hasElements ())
            return null;
        CRElement[] elements = new CRElement[_elements.size ()];
        _elements.copyInto (elements);
        return elements;
    }

    /**
     * getElementNames Returns a String array of the child element names.
     * 
     * @return an array of child element names for this element
     */
    public String[] getElementNames ()
    {
        CRElement el = null;

        if (!hasElements ())
            return null;

        String[] names = new String[_elements.size ()];
        for (int x = 0; x < _elements.size (); x++)
        {
            el = (CRElement) _elements.elementAt (x);
            names[x] = el.getElementName ();
        }
        return names;
    }

    /**
     * getElement Return the CRElement child object with a specific name.
     * 
     * @param elName the name of the child element to find
     * @return the element for the given name or null if not found
     */
    public CRElement getElement (String elName)
    {
        CRElement el = null;
        for (int x = 0; x < _elements.size (); x++)
        {
            el = (CRElement) _elements.elementAt (x);
            if (elName.equals (el.getElementName ()))
                return el;
        }
        return null;
    }

    /**
     * getNamedElementWithAttribute Returns the CRElement child object with the
     * specified attribute and attribute value.
     * 
     * @param elName the element name
     * @param the attribute name
     * @param the attribute value
     * @return the element or null if not found
     */
    public CRElement getNamedElementWithAttribute (String elName, String attr, String value)
    {
        CRElement el = null;
        for (int x = 0; x < _elements.size (); x++)
        {
            el = (CRElement) _elements.elementAt (x);
            if (el.getElementName ().equals (elName))
            {
                if (value.equals (el.getAttribute (attr)))
                    return el;
            }
        }
        return null;
    }

    /**
     * getElementWithAttribute Returns the CRElement child object with the
     * specified attribute and attribute value.
     * 
     * @param the name of the attribute
     * @param the value for the attribute
     * @return the element for the given attribute name and value or null if not
     *         found
     */
    public CRElement getElementWithAttribute (String attr, String value)
    {
        CRElement el = null;
        for (int x = 0; x < _elements.size (); x++)
        {
            el = (CRElement) _elements.elementAt (x);
            if (value.equals (el.getAttribute (attr)))
                return el;
        }
        return null;
    }

    /**
     * getAttribute Returns the attribute value of the specified attribute name.
     * 
     * @param key the attribute name
     * @return the value of the attribute or null if not found
     */
    public String getAttribute (String key)
    {

        if (_attributes.containsKey (key))
            return (String) _attributes.get (key);
        else
            return null;
    }

    /**
     * getAttributes Returns a string of the attributes name and value to be
     * displayed by the printElement method.
     * 
     * @return a string of the form name=value for the current element
     */
    public String getAttributes ()
    {
        StringBuffer buff = new StringBuffer ();
        Enumeration e = _attributes.keys ();
        String key;
        String value;
        while (e.hasMoreElements ())
        {
            key = (String) e.nextElement ();
            value = (String) _attributes.get (key);
            buff.append (key + " = " + value + "\n");
        }
        return buff.toString ();
    }

}
